create2-vickrey-contracts 是一個黑客松競賽的作品,它的特色是使用 create2 來達到匿名競標的效果。
Vickrey Auction 是彌封次價拍賣的別稱,競標者將自己的出價放進信封袋不讓別人看到,拍賣的贏家只需要支付第二高價。為什麼是第二高價?因為最高出價者其實只需出價比第二高價者多一元,同樣能競標到商品。
本篇的先備知識:
在之前介紹過的 Blind Auction 使用 Commit-Reveal Scheme 來隱藏競標者的出價,但競標者需要夠過超額抵押來隱藏自己的真實出價,此外一場拍賣的參與者地址與總量是公開的。
那如何用 create2 做到匿名競標?當用戶要競標時,使用 create2 獲得一個指定地址,然後將 ETH 打進去,此時在區塊鏈上這是一筆「打錢進新地址」的交易,它在某種程度上可以藏樹於林,沒有人知道這筆轉帳其實正在參與拍賣的競標。
直到拍賣的 reveal 階段,用戶呼叫合約上的 reveal()
,合約會部署你指定的 create2 地址,此時這場拍賣的參與者才顯露出來,合約一經部署就會將裡面的 ETH 轉到拍賣合約,成為用戶的出價,並記錄當前的最高與次高出價,等待所有人都 reveal 後完成拍賣。
競標者可否選擇不 reveal?不行,因為 create2 的地址來自於 contract creator,也就是說當你打錢進 create2 地址時(決定參與競標時),這個 create2 合約就必須由拍賣合約來部署,競標者無法自己部署把錢領回。
create2 address = keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]
有一個問題是,當用戶看到別人 reveal 的出價比自己高,於是在自己 reveal 前又送一點 ETH 到 create2 地址來增加自己的出價,這樣顯然不公平。
解決的方法是使用昨天介紹的 eth_getProof
,首先拍賣合約會在 reveal 階段開始時,將當下的 blockhash 存起來。用戶要 reveal 時,必須提供自己在這個 blockhash 時的餘額證明,因此在拍賣合約部署 create2 合約時,會先檢查 proof 取得用戶在該 blockhash 的歷史餘額,於是可確認 balance snapshot,這才是用戶真正的出價,若用戶在 reveal 階段主委加碼出價,使得 create2 內的 ETH 比 balance snapshot 來多,就把多出來的部分退還給用戶。
總地來說,拍賣合約上的 reveal()
首先驗證用戶的歷史餘額,然後再部署 create2 合約並將裡面的錢轉到拍賣合約,最後儲存合約當前的最高價、次高價與最高出價人的地址。
以下是程式碼片段:
function reveal(
address _bidder,
uint256 _bid,
bytes32 _subSalt,
uint256 _balAtSnapshot,
EthereumDecoder.BlockHeader memory _header,
MPT.MerkleProof memory _accountDataProof
) external returns (address bidAddr) {
uint256 totalBid;
{
if (revealStartBlock + 7200 < block.number) revert RevealOver();
bytes32 storedBlockHashCached = storedBlockHash;
if (storedBlockHashCached == bytes32(0)) revert NotYetReveal();
(bytes32 salt, address depositAddr) = getBidDepositAddr(
_bidder,
_bid,
_subSalt
);
if (
!_verifyProof(
_header,
_accountDataProof,
_balAtSnapshot,
depositAddr,
storedBlockHashCached
)
) revert InvalidProof();
uint256 balBefore = address(this).balance;
assembly {
mstore(0x00, BID_EXTRACTOR_CODE)
bidAddr := create2(
0,
BID_EXTRACTOR_CODE_OFFSET,
BID_EXTRACTOR_CODE_SIZE,
salt
)
}
totalBid = address(this).balance - balBefore;
}
uint256 actualBid = min(_bid, _balAtSnapshot);
uint256 bidderRefund = totalBid - actualBid;
uint128 topBidCached = topBid;
uint128 sndBidCached = sndBid;
address topBidderCached = topBidder;
if (actualBid > topBidCached) {
_asyncSend(topBidderCached, topBidCached);
topBidder = topBidderCached = _bidder;
sndBid = sndBidCached = uint128(topBidCached);
topBid = topBidCached = uint128(actualBid);
} else {
if (actualBid > sndBid) sndBid = sndBidCached = uint128(actualBid);
bidderRefund += actualBid;
}
_asyncSend(_bidder, bidderRefund);
emit BidRevealed(
topBidderCached,
_bidder,
topBidCached,
sndBidCached,
actualBid
);
}